package com.spring.mvc.mina.test.demo;

import java.io.IOException;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.UnknownHostException;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.SocketChannel;
import java.nio.channels.spi.SelectorProvider;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;

public class NioClient implements Runnable {
	
	private InetAddress hostAddress;
	private int port;
	
	private Selector selector;
	
	private ByteBuffer readBuffer=ByteBuffer.allocate(8192);
	
	private List<ChangeRequest> pendingChanges=new LinkedList<ChangeRequest>();

	private Map<SocketChannel, List<ByteBuffer>> pendingData=new HashMap<SocketChannel, List<ByteBuffer>>();
	
	private Map<SocketChannel,RspHandler> rspHandlers=Collections.synchronizedMap(new HashMap<SocketChannel,RspHandler>());
	
	public NioClient(InetAddress hostAddress,int port) throws IOException {
		this.hostAddress=hostAddress;
		this.port=port;
		this.selector=this.initSelector();
	}
	
	private void send(byte[] data,RspHandler handler) throws IOException {
		SocketChannel socket=this.initiateConnection();
		
		this.rspHandlers.put(socket, handler);
		synchronized (this.pendingData) {
			List<ByteBuffer> queue=this.pendingData.get(socket);
			if(queue==null){
				queue=new ArrayList<ByteBuffer>();
				this.pendingData.put(socket, queue);
			}
			queue.add(ByteBuffer.wrap(data));
		}
		this.selector.wakeup();
	}
	
	public void run() {
		while(true){
			try{
				synchronized (this.pendingChanges) {
					Iterator<ChangeRequest> changes=this.pendingChanges.iterator();
					while(changes.hasNext()){
						ChangeRequest change=changes.next();
						switch(change.type){
						case ChangeRequest.CHANGEOPS:
							SelectionKey key=change.socket.keyFor(this.selector);
							key.interestOps(change.ops);
							break;
						case ChangeRequest.REGISTER:
							change.socket.register(this.selector, change.ops);
							break;
						}
					}
					this.pendingChanges.clear();
				}
				this.selector.select();
				
				Iterator<SelectionKey> selectedKyes=this.selector.selectedKeys().iterator();
				while(selectedKyes.hasNext()){
					SelectionKey key=selectedKyes.next();
					selectedKyes.remove();
					
					if(!key.isValid()){
						continue;
					}
					if(key.isConnectable()){
						this.finishConnection(key);
					} else if(key.isReadable()){
						this.read(key);
					} else if(key.isWritable()){
						this.write(key);
					}
				}
			}catch(Exception e){
				e.printStackTrace();
			}
		}
	}

	private void read(SelectionKey key) throws IOException {
		SocketChannel socketChannel=(SocketChannel) key.channel();
		
		this.readBuffer.clear();
		int numRead=0;
		try{
			numRead=socketChannel.read(this.readBuffer);
		}catch(IOException e){
			key.cancel();
			socketChannel.close();
			return;
		}
		if(numRead==-1){
			key.channel().close();
			key.cancel();
			return;
		}
		
		this.handleResponse(socketChannel,this.readBuffer.array(),numRead);
	}
	
	private void handleResponse(SocketChannel socketChannel,byte[] data,int numRead) throws IOException{
		byte[] rspData=new byte[numRead];
		System.arraycopy(data, 0, rspData, 0, numRead);
		RspHandler handler=this.rspHandlers.get(socketChannel);
		if(handler.handleResponse(rspData)){
			socketChannel.close();
			socketChannel.keyFor(this.selector).cancel();
		}
	}

	private void write(SelectionKey key) throws IOException{
		SocketChannel socketChannel=(SocketChannel) key.channel();
		synchronized (this.pendingData) {
			List<ByteBuffer> queue=this.pendingData.get(socketChannel);
			while(!queue.isEmpty()){
				ByteBuffer buf=(ByteBuffer)queue.get(0);
				socketChannel.write(buf);
				if(buf.remaining()>0){
					break;
				}
				queue.remove(0);
			}
			if(queue.isEmpty()){
				key.interestOps(SelectionKey.OP_READ);
			}
		}
	}
	
	private void finishConnection(SelectionKey key) throws IOException{
		SocketChannel socketChannel=(SocketChannel)key.channel();
		try{
			socketChannel.finishConnect();
		} catch(IOException e){
			e.printStackTrace();
			key.cancel();
			return;
		}
		key.interestOps(SelectionKey.OP_WRITE);
	}
	
	private SocketChannel initiateConnection() throws IOException {
		SocketChannel socketChannel=SocketChannel.open();
		socketChannel.configureBlocking(false);
		socketChannel.connect(new InetSocketAddress(hostAddress, port));
		
		synchronized (this.pendingChanges) {
			this.pendingChanges.add(new ChangeRequest(socketChannel, ChangeRequest.REGISTER, SelectionKey.OP_CONNECT));
		}
		return socketChannel;
	}
	        
	private Selector initSelector() throws IOException{
		return SelectorProvider.provider().openSelector();
	}
	
	public static void main(String[] args) throws UnknownHostException, IOException {
		NioClient client=new NioClient(InetAddress.getLocalHost(), 80);
		Thread t=new Thread(client);
		t.setDaemon(true);
		t.start();
		RspHandler handler=new RspHandler();
		client.send("GET /HTTP/1.0\r\n\r\n".getBytes(),handler);
		handler.waitForResponse();
		
		//FileChannel fis=new FileInputStream("D:\\test.txt").getChannel();
	}
}
